#include <locale.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/stat.h>
#include <sys/time.h>

#define LKC_DIRECT_LINK
#include "lkc.h"

static void conf(struct menu *menu);
static void check_conf(struct menu *menu);

enum input_mode {
    oldaskconfig,
    silentoldconfig,
    oldconfig,
    allnoconfig,
    allyesconfig,
    allmodconfig,
    alldefconfig,
    randconfig,
    defconfig,
    savedefconfig,
    listnewconfig,
    oldnoconfig,
} input_mode = oldaskconfig;

char *defconfig_file;

static int conf_cnt;
static int indent = 1;
static int sync_kconfig;
static int valid_stdin = 1;

static char line[128];
static struct menu *rootEntry;

static void print_help(struct menu *menu)
{
    struct gstr help = str_new();

    menu_get_ext_help(menu, &help);

    printf("\n%s\n", str_get(&help));
    str_free(&help);
}

static void strip(char *str)
{
    int l;
    char *p = str;

    while ((isspace(*p))) {
        p++;
    }

    l = strlen(p);
    if (p != str) {
        memmove(str, p, l + 1);
    }

    if (!l) {
        return;
    }

    p = str + l - 1;
    while ((isspace(*p))) {
        *p-- = 0;
    }
}

static void check_stdin(void)
{
    if (!valid_stdin) {
        printf(_("aborted!\n\n"));
        printf(_("Console input/output is redirected. "));
        printf(_("Run 'make oldconfig' to update configuration.\n\n"));
        exit(1);
    }
}

static int conf_askvalue(struct symbol *sym, const char *def)
{
    enum symbol_type type = sym_get_type(sym);

    if (!sym_has_value(sym)) {
        printf(_("(NEW) "));
    }

    line[0] = '\n';
    line[1] = 0;

    if (!sym_is_changable(sym)) {
        printf("%s\n", def);
        line[0] = '\n';
        line[1] = 0;
        return 0;
    }

    switch (input_mode) {
        case oldconfig:
        case silentoldconfig:
            if (sym_has_value(sym)) {
                printf("%s\n", def);
                return 0;
            }
            check_stdin();

        case oldaskconfig:
            fflush(stdout);
            xfgets(line, 128, stdin);
            return 1;

        default:
            break;
    }

    switch (type) {
        case S_INT:
        case S_HEX:
        case S_STRING:
            printf("%s\n", def);
            return 1;

        default:
            ;
    }
    printf("%s", line);

    return 1;
}

static int conf_string(struct menu *menu)
{
    const char *def;
    struct symbol *sym = menu->sym;

    while (1) {
        printf("%*s%s ", indent - 1, "", _(menu->prompt->text));
        printf("(%s) ", sym->name);

        def = sym_get_string_value(sym);
        if (sym_get_string_value(sym)) {
            printf("[%s] ", def);
        }

        if (!conf_askvalue(sym, def)) {
            return 0;
        }

        switch (line[0]) {
            case '\n':
                break;

            case '?':
                /* print help */
                if (line[1] == '\n') {
                    print_help(menu);
                    def = NULL;
                    break;
                }

            default:
                line[strlen(line)-1] = 0;
                def = line;
        }

        if (def && sym_set_string_value(sym, def)) {
            return 0;
        }
    }
}

static int conf_sym(struct menu *menu)
{
    tristate oldval, newval;
    struct symbol *sym = menu->sym;

    while (1) {
        printf("%*s%s ", indent - 1, "", _(menu->prompt->text));
        if (sym->name) {
            printf("(%s) ", sym->name);
        }

        putchar('[');
        oldval = sym_get_tristate_value(sym);
        switch (oldval) {
            case no:
                putchar('N');
                break;

            case mod:
                putchar('M');
                break;

            case yes:
                putchar('Y');
                break;
        }

        if (oldval != no && sym_tristate_within_range(sym, no)) {
            printf("/n");
        }

        if (oldval != mod && sym_tristate_within_range(sym, mod)) {
            printf("/m");
        }

        if (oldval != yes && sym_tristate_within_range(sym, yes)) {
            printf("/y");
        }

        if (menu_has_help(menu)) {
            printf("/?");
        }
        printf("] ");

        if (!conf_askvalue(sym, sym_get_string_value(sym))) {
            return 0;
        }
        strip(line);

        switch (line[0]) {
            case 'n':
            case 'N':
                newval = no;
                if (!line[1] || !strcmp(&line[1], "o")) {
                    break;
                }
                continue;

            case 'm':
            case 'M':
                newval = mod;
                if (!line[1]) {
                    break;
                }
                continue;

            case 'y':
            case 'Y':
                newval = yes;
                if (!line[1] || !strcmp(&line[1], "es")) {
                    break;
                }
                continue;

            case 0:
                newval = oldval;
                break;

            case '?':
                goto help;

            default:
                continue;
        }

        if (sym_set_tristate_value(sym, newval)) {
            return 0;
        }
help:
        print_help(menu);
    }
}

static int conf_choice(struct menu *menu)
{
    bool is_new;
    struct menu *child;
    struct symbol *sym, *def_sym;

    sym = menu->sym;
    is_new = !sym_has_value(sym);
    if (sym_is_changable(sym)) {
        conf_sym(menu);
        sym_calc_value(sym);

        switch (sym_get_tristate_value(sym)) {
            case no:
                return 1;

            case mod:
                return 0;

            case yes:
                break;
        }
    } else {
        switch (sym_get_tristate_value(sym)) {
            case no:
                return 1;

            case mod:
                printf("%*s%s\n", indent - 1, "", _(menu_get_prompt(menu)));
                return 0;

            case yes:
                break;
        }
    }

    while (1) {
        int cnt, def;

        printf("%*s%s\n", indent - 1, "", _(menu_get_prompt(menu)));
        def_sym = sym_get_choice_value(sym);
        cnt = def = 0;
        line[0] = 0;
        for (child = menu->list; child; child = child->next) {
            if (!menu_is_visible(child)) {
                continue;
            }

            if (!child->sym) {
                printf("%*c %s\n", indent, '*', _(menu_get_prompt(child)));
                continue;
            }

            cnt++;
            if (child->sym == def_sym) {
                def = cnt;
                printf("%*c", indent, '>');
            } else {
                printf("%*c", indent, ' ');
            }

            printf(" %d. %s", cnt, _(menu_get_prompt(child)));
            if (child->sym->name) {
                printf(" (%s)", child->sym->name);
            }

            if (!sym_has_value(child->sym)) {
                printf(_(" (NEW)"));
            }
            printf("\n");
        }

        printf(_("%*schoice"), indent - 1, "");
        if (cnt == 1) {
            printf("[1]: 1\n");
            goto conf_childs;
        }

        printf("[1-%d", cnt);
        if (menu_has_help(menu)) {
            printf("?");
        }
        printf("]: ");

        switch (input_mode) {
            case oldconfig:
            case silentoldconfig:
                if (!is_new) {
                    cnt = def;
                    printf("%d\n", cnt);
                    break;
                }
                check_stdin();

            case oldaskconfig:
                fflush(stdout);
                xfgets(line, 128, stdin);
                strip(line);
                if (line[0] == '?') {
                    print_help(menu);
                    continue;
                }

                if (!line[0]) {
                    cnt = def;
                } else if (isdigit(line[0])) {
                    cnt = atoi(line);
                } else {
                    continue;
                }
                break;

            default:
                break;
        }

conf_childs:
        for (child = menu->list; child; child = child->next) {
            if (!child->sym || !menu_is_visible(child)) {
                continue;
            }

            if (!--cnt) {
                break;
            }
        }

        if (!child) {
            continue;
        }

        if (line[0] && (line[strlen(line) - 1] == '?')) {
            print_help(child);
            continue;
        }

        sym_set_choice_value(sym, child->sym);
        for (child = child->list; child; child = child->next) {
            indent += 2;
            conf(child);
            indent -= 2;
        }

        return 1;
    }
}

static void conf(struct menu *menu)
{
    struct symbol *sym;
    struct menu *child;
    struct property *prop;

    if (!menu_is_visible(menu)) {
        return;
    }

    sym = menu->sym;
    prop = menu->prompt;
    if (prop) {
        const char *prompt;

        switch (prop->type) {
            case P_MENU:
                if ((input_mode == silentoldconfig || input_mode == listnewconfig || input_mode == oldnoconfig) && rootEntry != menu) {
                    check_conf(menu);
                    return;
                }

            case P_COMMENT:
                prompt = menu_get_prompt(menu);
                if (prompt) {
                    printf("%*c\n%*c %s\n%*c\n", indent, '*', indent, '*', _(prompt), indent, '*');
                }

            default:
                ;
        }
    }

    if (!sym) {
        goto conf_childs;
    }

    if (sym_is_choice(sym)) {
        conf_choice(menu);
        if (sym->curr.tri != mod) {
            return;
        }
        goto conf_childs;
    }

    switch (sym->type) {
        case S_INT:
        case S_HEX:
        case S_STRING:
            conf_string(menu);
            break;

        default:
            conf_sym(menu);
            break;
    }

conf_childs:
    if (sym) {
        indent += 2;
    }

    for (child = menu->list; child; child = child->next) {
        conf(child);
    }

    if (sym) {
        indent -= 2;
    }
}

static void check_conf(struct menu *menu)
{
    struct symbol *sym;
    struct menu *child;

    if (!menu_is_visible(menu)) {
        return;
    }

    sym = menu->sym;
    if (sym && !sym_has_value(sym)) {
        if (sym_is_changable(sym) ||
            (sym_is_choice(sym) && sym_get_tristate_value(sym) == yes)) {
            if (input_mode == listnewconfig) {
                if (sym->name && !sym_is_choice_value(sym)) {
                    printf("%s%s\n", CONFIG_, sym->name);
                }
            } else if (input_mode != oldnoconfig) {
                if (!conf_cnt++) {
                    printf(_("*\n* Restart config...\n*\n"));
                }

                rootEntry = menu_get_parent_menu(menu);
                conf(rootEntry);
            }
        }
    }

    for (child = menu->list; child; child = child->next) {
        check_conf(child);
    }
}

static struct option long_opts[] = {
    {"oldaskconfig",    no_argument,       NULL, oldaskconfig},
    {"oldconfig",       no_argument,       NULL, oldconfig},
    {"silentoldconfig", no_argument,       NULL, silentoldconfig},
    {"defconfig",       optional_argument, NULL, defconfig},
    {"savedefconfig",   required_argument, NULL, savedefconfig},
    {"allnoconfig",     no_argument,       NULL, allnoconfig},
    {"allyesconfig",    no_argument,       NULL, allyesconfig},
    {"allmodconfig",    no_argument,       NULL, allmodconfig},
    {"alldefconfig",    no_argument,       NULL, alldefconfig},
    {"randconfig",      no_argument,       NULL, randconfig},
    {"listnewconfig",   no_argument,       NULL, listnewconfig},
    {"oldnoconfig",     no_argument,       NULL, oldnoconfig},
    {NULL, 0, NULL, 0}
};

int main(int ac, char **av)
{
    int opt;
    const char *name;
    struct stat tmpstat;

    setlocale(LC_ALL, "");
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);

    while ((opt = getopt_long(ac, av, "", long_opts, NULL)) != -1) {
        input_mode = (enum input_mode)opt;
        switch (opt) {
            case silentoldconfig:
                sync_kconfig = 1;
                break;

            case defconfig:
            case savedefconfig:
                defconfig_file = optarg;
                break;

            case randconfig: {
                struct timeval now;
                unsigned int seed;

                /*
                * Use microseconds derived seed,
                * compensate for systems where it may be zero
                */
                gettimeofday(&now, NULL);

                seed = (unsigned int)((now.tv_sec + 1) * (now.tv_usec + 1));
                srand(seed);
                break;
            }

            case '?':
                fprintf(stderr, _("See README for usage info\n"));
                exit(1);
                break;
        }
    }

    if (ac == optind) {
        printf(_("%s: Kconfig file missing\n"), av[0]);
        exit(1);
    }

    name = av[optind];
    conf_parse(name);
    //zconfdump(stdout);
    if (sync_kconfig) {
        name = conf_get_configname();
        if (stat(name, &tmpstat)) {
            fprintf(stderr, _("***\n"
                "*** Configuration file \"%s\" not found!\n"
                "***\n"
                "*** Please run some configurator (e.g. \"make oldconfig\" or\n"
                "*** \"make menuconfig\" or \"make xconfig\").\n"
                "***\n"), name);
            exit(1);
        }
    }

    switch (input_mode) {
        case defconfig:
            if (!defconfig_file) {
                defconfig_file = conf_get_default_confname();
            }

            if (conf_read(defconfig_file)) {
                printf(_("***\n"
                    "*** Can't find default configuration \"%s\"!\n"
                    "***\n"), defconfig_file);
                exit(1);
            }
            break;

        case savedefconfig:
        case silentoldconfig:
        case oldaskconfig:
        case oldconfig:
        case listnewconfig:
        case oldnoconfig:
            conf_read(NULL);
            break;

        case allnoconfig:
        case allyesconfig:
        case allmodconfig:
        case alldefconfig:
        case randconfig:
            name = getenv("KCONFIG_ALLCONFIG");
            if (name && !stat(name, &tmpstat)) {
                conf_read_simple(name, S_DEF_USER);
                break;
            }

            switch (input_mode) {
                case allnoconfig:   name = "allno.config"; break;
                case allyesconfig:  name = "allyes.config"; break;
                case allmodconfig:  name = "allmod.config"; break;
                case alldefconfig:  name = "alldef.config"; break;
                case randconfig:    name = "allrandom.config"; break;
                default: break;
            }

            if (!stat(name, &tmpstat)) {
                conf_read_simple(name, S_DEF_USER);
            } else if (!stat("all.config", &tmpstat)) {
                conf_read_simple("all.config", S_DEF_USER);
            }
            break;

        default:
            break;
    }

    if (sync_kconfig) {
        if (conf_get_changed()) {
            name = getenv("KCONFIG_NOSILENTUPDATE");
            if (name && *name) {
                fprintf(stderr, _("\n*** The configuration requires explicit update.\n\n"));
                return 1;
            }
        }
        valid_stdin = isatty(0) && isatty(1) && isatty(2);
    }

    switch (input_mode) {
        case allnoconfig:
            conf_set_all_new_symbols(def_no);
            break;

        case allyesconfig:
            conf_set_all_new_symbols(def_yes);
            break;

        case allmodconfig:
            conf_set_all_new_symbols(def_mod);
            break;

        case alldefconfig:
            conf_set_all_new_symbols(def_default);
            break;

        case randconfig:
            conf_set_all_new_symbols(def_random);
            break;

        case defconfig:
            conf_set_all_new_symbols(def_default);
            break;

        case savedefconfig:
            break;

        case oldaskconfig:
            rootEntry = &rootmenu;
            conf(&rootmenu);
            input_mode = silentoldconfig;
            /* fall through */

        case oldconfig:
        case listnewconfig:
        case oldnoconfig:
        case silentoldconfig:
            /* Update until a loop caused no more changes */
            do {
                conf_cnt = 0;
                check_conf(&rootmenu);
            } while (conf_cnt && (input_mode != listnewconfig && input_mode != oldnoconfig));
            break;
    }

    if (sync_kconfig) {
        /* silentoldconfig is used during the build so we shall update autoconf.
         * All other commands are only used to generate a config.
         */
        if (conf_get_changed() && conf_write(NULL)) {
            fprintf(stderr, _("\n*** Error during writing of the configuration.\n\n"));
            exit(1);
        }

        if (conf_write_autoconf()) {
            fprintf(stderr, _("\n*** Error during update of the configuration.\n\n"));
            return 1;
        }
    } else if (input_mode == savedefconfig) {
        if (conf_write_defconfig(defconfig_file)) {
            fprintf(stderr, _("n*** Error while saving defconfig to: %s\n\n"), defconfig_file);
            return 1;
        }
    } else if (input_mode != listnewconfig) {
        if (conf_write(NULL)) {
            fprintf(stderr, _("\n*** Error during writing of the configuration.\n\n"));
            exit(1);
        }
    }

    return 0;
}

/*
 * Helper function to facilitate fgets() by Jean Sacren.
 */
void xfgets(char *str, int size, FILE *in)
{
    if (fgets(str, size, in) == NULL) {
        fprintf(stderr, "\nError in reading or end of file.\n");
    }
}
